現在使用者已經可以透過右上角 UI 控制整個太陽系。
今天要進一步把操作方式從操作 UI 變成直接操作 3D Scene
也就是讓使用者可以直接用滑鼠點擊行星
當某顆行星被選中時,它所對應的軌道會變色,而其他軌道維持原本的灰色。
由於目前專案程式碼已逐漸增加,從 Day 23 開始文章只列出當日新增或修改的程式碼。
完整整合版將於 Day 30 最終完成時提供。
今天會完成:
項目 修改內容
Planet Orbit 除了 Orbit 本身,再保存 Planet Name
Raycaster 新增 THREE.Raycaster()
Mouse 新增 THREE.Vector2()
Click 判斷使用者點到哪顆行星
Orbit Color 選中的軌道改成黃色
Empty Space 點擊空白區域取消選取
| 項目 | 修改內容 |
|---|---|
| Planet Orbit | 除了 Orbit 本身,再保存 Planet Name |
| Raycaster | 新增 THREE.Raycaster() |
| Mouse | 新增 THREE.Vector2() |
| Click | 判斷使用者點到哪顆行星 |
| Orbit Color | 選中的軌道改成黃色 |
| Empty Space | 點擊空白區域取消選取 |
今天完成之後原本所有 Orbit → 灰色
點擊 Earth Orbit:
Mercury Orbit → 灰色
Venus Orbit → 灰色
Earth Orbit → 黃色
Mars Orbit → 灰色
...
今天最重要的新工具 THREE.Raycaster
可以把它想像成:
Camera
↓
滑鼠點擊位置
↓
發射一條看不見的射線
↓
穿過 3D Scene
↓
檢查碰到了哪些 Object
如果這條 Ray 穿過 Earth,就代表:
使用者點到了 Earth
理解它是麼之後,我們先建立 Raycaster 首先要新增:
const raycaster =
new THREE.Raycaster();
接著建立一個用來保存滑鼠位置的:
const mouse =
new THREE.Vector2();
滑鼠只需要 X Y 不需要 Z
所以使用 THREE.Vector2 而不是 THREE.Vector3
瀏覽器取得的 Mouse Position 通常是:
左上角 (0, 0)
例如螢幕寬度:1920
滑鼠可能是 X = 960 Y = 500
但 Raycaster 不直接使用這個座標。
Three.js 要使用的是:
X = -1 ~ 1
Y = -1 ~ 1
這種座標叫做 NDC(Normalized Device Coordinates)
先取得 Canvas 的範圍:
const rect =
renderer.domElement
.getBoundingClientRect();
接著:
mouse.x =
(
(
event.clientX -
rect.left
) /
rect.width
) *
2 -
1;
Y 比較特別:
mouse.y =
-(
(
event.clientY -
rect.top
) /
rect.height
) *
2 +
1;
注意 mouse.y 前面有 '-'
原因是瀏覽器 Y → 往下增加
Three.js NDC Y → 往上增加
方向剛好相反。
完成 Mouse Position 後:
raycaster.setFromCamera(
mouse,
camera
);
意思就是 mouse + camera 建立 → Ray
這條 Ray 會從 Camera 出發,穿過滑鼠點擊的位置。
我們不需要讓 Raycaster 檢查:
Sun
Orbit
Stars
Moon
Saturn Ring
Venus Atmosphere
今天只想選擇 八大行星
而我們本來就有:
const planets =
[];
裡面每個 Planet 都保存:
{
name:
data.name,
mesh:
planet,
...
}
所以可以取得:
const planetMeshes =
planets.map(
(planet) =>
planet.mesh
);
得到:
[
Mercury Mesh,
Venus Mesh,
Earth Mesh,
Mars Mesh,
Jupiter Mesh,
Saturn Mesh,
Uranus Mesh,
Neptune Mesh
]
使用:
const intersects =
raycaster.intersectObjects(
planetMeshes,
false
);
如果 intersects.length > 0 代表有點到 Planet。
第一個碰到的 intersects[0] 通常就是離 Camera 最近的 Object。
所以:
const selectedMesh =
intersects[0].object;
我們取得的是 Mesh
但真正需要知道的是
它是 Earth? Mars? Saturn?
所以可以:
const selectedPlanet =
planets.find(
(planet) =>
planet.mesh ===
selectedMesh
);
這樣 selectedPlanet.name 就可能得到 Earth 或 Saturn
Day 24 為了控制 Orbit 顯示與隱藏,我們原本保存:
planetOrbits.push(
orbit
);
今天需要知道這條 Orbit 是誰的?
所以把它改成:
planetOrbits.push({
name:
data.name,
line:
orbit
});
因為以前 planetOrbits 裡面直接放 LineLoop
現在變成 Object 所以原本:
orbit.visible =
orbitToggle.checked;
要改成:
orbit.line.visible =
orbitToggle.checked;
完整:
planetOrbits.forEach(
(orbit) => {
orbit.line.visible =
orbitToggle.checked;
}
);
今天準備兩個顏色:
const defaultOrbitColor =
0x444444;
const selectedOrbitColor =
0xffcc66;
你之後想換成 藍色紅色綠色 都可以。
建立:
function resetOrbitColors() {
planetOrbits.forEach(
(orbit) => {
orbit.line.material.color.setHex(
defaultOrbitColor
);
}
);
}
每次點擊新的 Planet 前 resetOrbitColors();
所以上一顆選中的 Orbit 恢復灰色
再建立:
function highlightOrbit(
planetName
) {
const orbit =
planetOrbits.find(
(orbit) =>
orbit.name ===
planetName
);
if (
orbit
) {
orbit.line.material.color.setHex(
selectedOrbitColor
);
}
}
例如:
highlightOrbit(
"Earth"
);
Earth Orbit 就會變黃色。
今天直接監聽:
renderer.domElement.addEventListener(
"pointerdown",
...
);
而不是 window.addEventListener(...)
原因是我們只希望 點擊 3D Canvas 才觸發 Raycaster。
這樣點擊右上角 UI 選單就不會意外選到 Canvas 後面的 Planet。
今天最重要的新程式是:
renderer.domElement.addEventListener(
"pointerdown",
(event) => {
const rect =
renderer.domElement
.getBoundingClientRect();
mouse.x =
(
(
event.clientX -
rect.left
) /
rect.width
) *
2 -
1;
mouse.y =
-(
(
event.clientY -
rect.top
) /
rect.height
) *
2 +
1;
raycaster.setFromCamera(
mouse,
camera
);
const planetMeshes =
planets.map(
(planet) =>
planet.mesh
);
const intersects =
raycaster.intersectObjects(
planetMeshes,
false
);
resetOrbitColors();
if (
intersects.length >
0
) {
const selectedMesh =
intersects[0].object;
const selectedPlanet =
planets.find(
(planet) =>
planet.mesh ===
selectedMesh
);
if (
selectedPlanet
) {
highlightOrbit(
selectedPlanet.name
);
console.log(
"Selected:",
selectedPlanet.name
);
}
}
}
);
這裡有一個小細節。
我們在判斷以前就先執行 resetOrbitColors();
因此點 Earth → Earth Orbit 黃色
接著點空白resetOrbitColors() → 全部恢復灰色
沒有 intersect → 沒有新的 Highlight
所以就完成取消選取不用另外做按鈕。
今天其實也可以讓 Planet 本身發光、放大、改顏色
但我們先不要一次加入太多效果。
Day 26 的目的主要是學會:
Mouse
↓
Raycaster
↓
3D Object
所以今天用 Orbit Highlight
當作最明顯的選取回饋。
今天最重要的新工具就是 THREE.Raycaster
以及 raycaster.intersectObjects()
現在使用者已經可以
點 Mercury → Mercury Orbit Highlight
點 Earth → Earth Orbit Highlight
而其他 Orbit 維持灰色
這就完成了第一個真正的:
3D Object Selection
「Planet Focus × Planet Info:鎖定行星與資訊面板」
今天我們已經知道使用者點了哪顆 Planet
Day 27 就可以把這個資訊真正拿來使用。
例如點擊Earth → Camera 會鎖定 Earth
而且 Earth 還在公轉,所以 Camera 的 Target 必須每一幀跟著 Earth 更新
接著 UI 也會顯示 Earth
中文:地球
距離太陽:第三顆行星
衛星:Moon
公轉速度:0.8
並加入:返回太陽系
讓 Camera 回到原本的:Sun Center Mode
Day 27 會是目前互動效果最大的一次升級。